Java内存溢出(OOM)异常完全指南 您所在的位置:网站首页 Java oom异常的原因分析 Java内存溢出(OOM)异常完全指南

Java内存溢出(OOM)异常完全指南

2024-06-10 00:32| 来源: 网络整理| 查看: 265

转自Java内存溢出(OOM)异常完全指南

我的职业生涯中见过数以千计的内存溢出异常均与下文中的8种情况相关。本文分析什么情况会导致这些异常出现,提供示例代码的同时为您提供解决指南。 Nikita Salnikov-Tarnovski Plumbr Co-Founder and VP of Engineering 本文内容来源于Plumbr,对原文内容有删减和补充

这也许是目前最为完整的Java OOM异常的解决指南。

1、java.lang.OutOfMemoryError:Java heap space

Java应用程序在启动时会指定所需要的内存大小,它被分割成两个不同的区域:Heap space(堆空间)和Permgen(永久代):

JVM内存模型示意图

这两个区域的大小可以在JVM(Java虚拟机)启动时通过参数-Xmx和-XX:MaxPermSize设置,如果你没有显式设置,则将使用特定平台的默认值。

当应用程序试图向堆空间添加更多的数据,但堆却没有足够的空间来容纳这些数据时,将会触发java.lang.OutOfMemoryError: Java heap space异常。需要注意的是:即使有足够的物理内存可用,只要达到堆空间设置的大小限制,此异常仍然会被触发。

原因分析

触发java.lang.OutOfMemoryError: Java heap space最常见的原因就是应用程序需要的堆空间是XXL号的,但是JVM提供的却是S号。解决方法也很简单,提供更大的堆空间即可。除了前面的因素还有更复杂的成因:

流量/数据量峰值:应用程序在设计之初均有用户量和数据量的限制,某一时刻,当用户数量或数据量突然达到一个峰值,并且这个峰值已经超过了设计之初预期的阈值,那么以前正常的功能将会停止,并触发java.lang.OutOfMemoryError: Java heap space异常。 内存泄漏:特定的编程错误会导致你的应用程序不停的消耗更多的内存,每次使用有内存泄漏风险的功能就会留下一些不能被回收的对象到堆空间中,随着时间的推移,泄漏的对象会消耗所有的堆空间,最终触发java.lang.OutOfMemoryError: Java heap space错误。 示例 ①、简单示例

首先看一个非常简单的示例,下面的代码试图创建2 x 1024 x 1024个元素的整型数组,当你尝试编译并指定12M堆空间运行时(java -Xmx12m OOM)将会失败并抛出java.lang.OutOfMemoryError: Java heap space错误,而当你指定13M堆空间时,将正常的运行。

class OOM { static final int SIZE=2*1024*1024; public static void main(String[] a) { int[] i = new int[SIZE]; } }

运行如下:

D:\>javac OOM.java D:\>java -Xmx12m OOM Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at OOM.main(OOM.java:4) D:\>java -Xmx13m OOM ②、内存泄漏示例

在Java中,当开发者创建一个新对象(比如:new Integer(5))时,不需要自己开辟内存空间,而是把它交给JVM。在应用程序整个生命周期类,JVM负责检查哪些对象可用,哪些对象未被使用。未使用对象将被丢弃,其占用的内存也将被回收,这一过程被称为垃圾回收。JVM负责垃圾回收的模块集合被称为垃圾回收器(GC)。

Java的内存自动管理机制依赖于GC定期查找未使用对象并删除它们。Java中的内存泄漏是由于GC无法识别一些已经不再使用的对象,而这些未使用的对象一直留在堆空间中,这种堆积最终会导致java.lang.OutOfMemoryError: Java heap space错误。

我们可以非常容易的写出导致内存泄漏的Java代码:

public class KeylessEntry { static class Key { Integer id; Key(Integer id) { this.id = id; } @Override public int hashCode() { return id.hashCode(); } } public static void main(String[] args) { Map m = new HashMap(); while(true) { for(int i=0;i= 0; i--) { try { int[] arr = new int[Integer.MAX_VALUE-i]; System.out.format("Successfully initialized an array with %,d elements.\n", Integer.MAX_VALUE-i); } catch (Throwable t) { t.printStackTrace(); } }

该示例重复四次,并在每个回合中初始化一个长原语数组。 该程序尝试初始化的数组的大小在每次迭代时增加1,最终达到Integer.MAX_VALUE。 现在,当使用Hotspot 7在64位Mac OS X上启动代码片段时,应该得到类似于以下内容的输出:

java.lang.OutOfMemoryError: Java heap space at eu.plumbr.demo.ArraySize.main(ArraySize.java:8) java.lang.OutOfMemoryError: Java heap space at eu.plumbr.demo.ArraySize.main(ArraySize.java:8) java.lang.OutOfMemoryError: Requested array size exceeds VM limit at eu.plumbr.demo.ArraySize.main(ArraySize.java:8) java.lang.OutOfMemoryError: Requested array size exceeds VM limit at eu.plumbr.demo.ArraySize.main(ArraySize.java:8)

注意,在出现Requested array size exceeded VM limit之前,出现了更熟悉的java.lang.OutOfMemoryError: Java heap space。 这是因为初始化2 ^ 31-1个元素的数组需要腾出8G的内存空间,大于JVM使用的默认值。

解决方案

java.lang.OutOfMemoryError:Requested array size exceeds VM limit可能会在以下任一情况下出现:

数组增长太大,最终大小在平台限制和Integer.MAX_INT之间 你有意分配大于2 ^ 31-1个元素的数组

在第一种情况下,检查你的代码库,看看你是否真的需要这么大的数组。也许你可以减少数组的大小,或者将数组分成更小的数据块,然后分批处理数据。

在第二种情况下,记住Java数组是由int索引的。因此,当在平台中使用标准数据结构时,数组不能超过2 ^ 31-1个元素。事实上,在编译时就会出错:error:integer number too large。

8、Out of memory:Kill process or sacrifice child

为了理解这个错误,我们需要补充一点操作系统的基础知识。操作系统是建立在进程的概念之上,这些进程在内核中作业,其中有一个非常特殊的进程,名叫“内存杀手(Out of memory killer)”。当内核检测到系统内存不足时,OOM killer被激活,然后选择一个进程杀掉。哪一个进程这么倒霉呢?选择的算法和想法都很朴实:谁占用内存最多,谁就被干掉。如果你对OOM Killer感兴趣的话,建议你阅读参考资料2中的文章。

OOM Killer,图片来源:plumbr

当可用虚拟虚拟内存(包括交换空间)消耗到让整个操作系统面临风险时,就会产生Out of memory:Kill process or sacrifice child错误。在这种情况下,OOM Killer会选择“流氓进程”并杀死它。

原因分析

默认情况下,Linux内核允许进程请求比系统中可用内存更多的内存,但大多数进程实际上并没有使用完他们所分配的内存。这就跟现实生活中的宽带运营商类似,他们向所有消费者出售一个100M的带宽,远远超过用户实际使用的带宽,一个10G的链路可以非常轻松的服务100个(10G/100M)用户,但实际上宽带运行商往往会把10G链路用于服务150人或者更多,以便让链路的利用率更高,毕竟空闲在那儿也没什么意义。

Linux内核采用的机制跟宽带运营商差不多,一般情况下都没有问题,但当大多数应用程序都消耗完自己的内存时,麻烦就来了,因为这些应用程序的内存需求加起来超出了物理内存(包括 swap)的容量,内核(OOM killer)必须杀掉一些进程才能腾出空间保障系统正常运行。就如同上面的例子中,如果150人都占用100M的带宽,那么总的带宽肯定超过了10G这条链路能承受的范围。

示例

当你在Linux上运行如下代码:

public static void main(String[] args){ List l = new java.util.ArrayList(); for (int i = 10000; i < 100000; i++) { try { l.add(new int[100000000]); } catch (Throwable t) { t.printStackTrace(); } } }

在Linux的系统日志中/var/log/kern.log会出现以下日志:

Jun 4 07:41:59 plumbr kernel: [70667120.897649] Out of memory: Kill process 29957 (java) score 366 or sacrifice child Jun 4 07:41:59 plumbr kernel: [70667120.897701] Killed process 29957 (java) total-vm:2532680kB, anon-rss:1416508kB, file-rss:0kB

注意:你可能需要调整交换文件和堆大小,否则你将很快见到熟悉的Java heap space异常。在原作者的测试用例中,使用-Xmx2g指定的2g堆,并具有以下交换配置:

# 注意:原作者使用,由于我手里并没有Linux环境,所以并未测试 swapoff -a dd if=/dev/zero of=swapfile bs=1024 count=655360 mkswap swapfile swapon swapfile 解决方案

解决这个问题最有效也是最直接的方法就是升级内存,其他方法诸如:调整OOM Killer配置、水平扩展应用,将内存的负载分摊到若干小实例上..... 我们不建议的做法是增加交换空间,具体原因已经在前文说过。参考资料②中详细的介绍了怎样微调OOM Killer配置以及OOM Killer选择进程算法的实现,建议你参考阅读。

参考资料:

① 想要了解更多PermGen与Metaspace的内容推荐你阅读:

Java 8会解决PermGen OutOfMemoryError问题吗? Java PermGen 去哪里了?

② 如果你对OOM Killer感兴趣的话,强烈建议你阅读这篇文章:

理解和配置 Linux 下的 OOM Killer

备注:水平有限,难免疏漏,如果问题请留言 本文已经同步更新到微信公众号:轻描淡写CODE » Java内存溢出(OOM)异常完全指南

转自Java内存溢出(OOM)异常完全指南



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有